package containerlogistik.implementation;

import static com.google.common.base.Preconditions.*;
import containerlogistik.interfaces.Bounded3DimStack;
import containerlogistik.interfaces.StowageLocation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
 * Abstract implementation of a 3d-stack of objects.
 */
public class Bounded3DimStackImpl<E> implements Bounded3DimStack<E> {
    
    StowageLocation stowageLoc;
    List<List<List<E>>> contents;
    
    private Bounded3DimStackImpl(StowageLocation stowageLoc) {
        this.stowageLoc = stowageLoc;
        // Initialize stack with fix capacity
        this.contents = new ArrayList<>(stowageLoc.bay());
        for (int bayIndex = 0; bayIndex < stowageLoc.bay(); bayIndex++){
            this.contents.add(bayIndex, new ArrayList<List<E>>(stowageLoc.row()));
            for (int rowIndex = 0; rowIndex < stowageLoc.row(); rowIndex++){
                this.contents.get(bayIndex).add(rowIndex, new ArrayList<E>(stowageLoc.tier()));     
            }
        }
    }
    
    static Bounded3DimStack valueOf(StowageLocation stowageLoc) {
        return new Bounded3DimStackImpl(stowageLoc);        
    }
    
    StowageLocation stowageLoc() {
        return this.stowageLoc;
    }
    
    List<List<List<E>>> contents() {
        return this.contents;
    }

    /**
     * Load one element to a specific location in stack.
     * @param bayNo
     * @param rowNo
     * @param elem 
     */
    @Override
    public void load(int bayNo, int rowNo, E elem) {
        checkArgument(bayNo <= this.stowageLoc().bay() && rowNo <= this.stowageLoc().row(), "Cannot load this element to a not existing place.");
        checkArgument(!this.tierIsFull(bayNo, rowNo), "Cannot load this element, because the tier is full.");     
        this.contents().get(bayNo).get(rowNo).add(elem);            
    }

    /**
     * Load one element to any location in stack.
     * @param elem element
     */
    @Override
    public void load(E elem) {
        checkArgument(!this.isFull(), "Cannot load this element, because the stack is full.");
        for(int bayIndex = 0; bayIndex < this.stowageLoc().bay(); bayIndex++) {
            for(int rowIndex = 0; rowIndex < this.stowageLoc().row(); rowIndex++) {
                if(!this.tierIsFull(bayIndex, rowIndex)) {
                    this.load(bayIndex, rowIndex, elem);
                    return;
                }
            }
        }
    }

    /**
     * Load all elements of coll in stack.
     * @TODO Optimize method
     * @param coll elements
     */
    @Override
    public void loadAll(Collection<? extends E> coll) {
        checkArgument(!this.isFull(), "Cannot load this elements, because the stack is full.");
        checkArgument(this.stowageLoc().capacity() >= coll.size(), "Cannot load this elements, because there is not enouth capacity in the stowage.");
        for(E elem : coll) {
            this.load(elem);
        }
    }
    
    /* Predicates */

    @Override
    public boolean isEmpty() {
        for(int bayIndex = 0; bayIndex < this.stowageLoc().bay(); bayIndex++) {
            for(int rowIndex = 0; rowIndex < this.stowageLoc().row(); rowIndex++) {
                if(!this.contents().get(bayIndex).get(rowIndex).isEmpty()) {
                    return false;
                }
            }
        }
        return true;
    }

    @Override
    public boolean isFull() {
        return this.getAll().size() >= this.stowageLoc().capacity();
    }

    @Override
    public boolean tierIsEmpty(int bay, int row) {        
        if(this.stowageLoc().bay() >= bay && this.stowageLoc().row() >= row) {
            return this.contents().get(bay).get(row).isEmpty();
        }
        return false;
    }

    @Override
    public boolean tierIsFull(int bay, int row) {
        if(this.stowageLoc().bay() >= bay && this.stowageLoc().row() >= row) {
            return this.contents().get(bay).get(row).size() == this.stowageLoc().tier();
        }
        return true;
    }

    @Override
    public boolean contains(Object elem) {
        return this.getAll().contains((E)elem);
    }

    @Override
    public boolean containsAll(Collection<?> coll) {
        return this.getAll().containsAll(coll);
    }

    /**
     * Returns the element at given location.
     * @param loc location
     * @return element
     */
    @Override
    public E get(StowageLocation loc) {
        checkArgument(loc.bay() <= stowageLoc().bay() && 
                loc.row() <= stowageLoc().row() && 
                loc.tier() <= stowageLoc().tier(), 
                "Cannot get element out of stowage location.");
        return this.contents().get(loc.bay()).get(loc.row()).get(loc.tier());
    }

    /**
     * Returns a Set<Element> with all elements on the 3d-stack.
     * @return Set of all elements on the stack.
     */
    @Override
    public Set<E> getAll() {
        HashSet set = new HashSet();
        for(int bayIndex = 0; bayIndex < this.stowageLoc().bay(); bayIndex++) {
            for(int rowIndex = 0; rowIndex < this.stowageLoc().row(); rowIndex++) {
                set.addAll(this.contents().get(bayIndex).get(rowIndex));
            }
        }
        return set;
    }

    /**
     * Search for an element on the stack and returns the location of the element
     * @param elem element
     * @return Loc location of the element.
     */
    @Override
    public StowageLocation locationOf(E elem) {
        for(int bayIndex = 0; bayIndex < this.stowageLoc().bay(); bayIndex++) {
            for(int rowIndex = 0; rowIndex < this.stowageLoc().row(); rowIndex++) {
                for(int tierIndex = 0; tierIndex < this.stowageLoc().tier(); tierIndex++) {
                    if(this.contents().get(bayIndex).get(rowIndex).get(tierIndex) == elem) {
                        return Entities.stowageLocation(bayIndex, rowIndex, tierIndex);
                    }
                }
            }
        }
        return Entities.nullLocation();
    }

    public boolean equals(Object obj) {
        if (obj == this) return true;
        if (!(obj instanceof Bounded3DimStack)) return false;
        return this.contents().equals(((Bounded3DimStackImpl)obj).contents()) &&
                this.stowageLoc().equals(((Bounded3DimStackImpl)obj).stowageLoc());
    }

    public int hashCode() {
        return Objects.hash(this.stowageLoc(), this.contents());
    }
    
}
